개요

이 포스팅의 내용은 <<자바 병렬 프로그래밍>> 의 3장 객체 공유 챕터를 요약한 내용이다.

객체 공유

이번 장에서는 여러 개의 스레드에서 특정 객체를 동시에 사용하려 할 때 섞이지 않고 안전하게 동작하도록 객체를 공유하고 공개하는 방법을 살펴본다.

소스 코드의 특정 블록을 동기화시키고자 할 때, 항상 메모리 가시성 문제가 발생하게 된다. 그래서 특정 변수의 값을 사용하고 있을 때 다른 스레드가 해당 변수의 값을 사용하지 못하도록 막아야 할 뿐만 아니라, 값을 사용한 다음 동기화 블록을 빠져나가고 나면 다른 스레드가 변경된 값을 즉시 사용할 수 있게 해야 한다.

가시성

특정 값을 변수에 쓴 다음에, 그 값을 다시 읽으면 이전에 저장해뒀던 바로 그 값을 가져오게 된다. 대부분 이런 경우가 정상적이지만, 멀티스레드 환경에서는 그렇지 않을 수 있다.

public class NoVisibility {
 
	private static boolean ready;
	private static int number;
 
	private static class ReaderThread extends Thread {
		public void run() {
			while(!ready) Thread.yield();
 
			System.out.println(number);
		}
	}
 
	public static void main(String [] args) {
		new ReaderThread().start();
		ready = true;
		number = 42;
	}
}

이 코드에서는 메인 스레드와 읽기 스레드가 동시에 ready 와 number 변수를 공유하고 있다. 읽기 스레드는 while 문을 돌면서 ready 가 true 가 될때까지 대기하고 있다가, true 가 되면 number 를 프린트한다. 메인 스레드에서는 number 와 ready 의 값을 변경한다.

자연스럽게 42가 출력될 거라고 예상할 수 있다. 그러나, 0이라는 값을 출력할 수도 있고 심지어 영원히 값을 출력하지 않을 수도 있다. 두 개의 스레드에서 변수를 공유하여 사용하고 있지만, 적절한 동기화 기법을 사용하지 않은 것이다.

이 현상은 흔히 재배치(reordering) 라고 하는 현상 때문에 발생한다. 재배치 현상은 특정 메소드의 소스코드가 100% 코딩된 순서로 동작한다는 점을 보장할 수 없다는 점에서 기인하는 문제이다. 메인 스레드는 number > ready 순으로 값을 세팅하지만, 읽기 스레드 입장에서는 ready > number 순으로 세팅될 수도 있고, 심지어는 아예 변경된 값을 읽지 못할 수도 있다.

이 문제를 해결하려면 다음 방법을 사용해야 한다.

여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 적용한다

스테일 데이터

앞에 예제에서 발생하는 현상을 스테일(stale : 낡은) 데이터라고 한다. 즉, 읽기 스레드가 ready 변수의 값을 읽으려고 할 때 변수가 이미 최신 값이 아닌 것이다. 이 문제는 항상 발생하는 것도 아니다. 그 이야기는 특정 스레드가 어떤 값을 사용할 때 그 값이 최신값일 수도 있고, 아닐수도 있다는 의미이다.

다음 예제로 확인해 보자

@NotThreadSafe
public class MutableInteger {
	private int value;
 
	public int get() {
		return value;
	}
	public void set(int value) {
		this.value = value;
	}
}

위의 클래스에서도 value 라는 값을 get / set 에서 동시에 사용하고 있지만 동기화가 되어 있지 않다. 이는 스테일 현상을 발생시킬 수도 있다. 특정 스레드가 set 메소드를 호출하고 다른 스레드가 get 메소드를 호출할 때, set 메소드에서 지정한 값을 읽어오지 못하는 것이다.

다음과 같이 코드를 수정하여야 한다.

@NotThreadSafe
public class MutableInteger {
	@GuardedBy("this") private int value;
 
	public synchronized int get() {
		return value;
	}
	public synchronized void set(int value) {
		this.value = value;
	}
}

이 때 set 메소드만 동기화 한다고 하면, get 메소드가 여전히 스테일 상황을 초래할 수 있다. 반드시 둘 다 동기화하여야 한다.

단일하지 않은 64비트 연산

위의 예제에서 확인해 볼 수 있듯이, 스테일 현상이 발생하더라도 아예 없는 값을 가져가는 것은 아니다. (오래된 값을 가져갈 수는 있지만) 그러나, 64비트를 사용하는 숫자형에 volatile 키워드를 사용하지 않을 때는 난대없는 값이 생길 수도 있다.

volatile 로 지정되지 않은 long 이나 double 형의 64비트 값은 메모리에 쓰거나 읽을 때 두 번의 32비트 연산을 사용할 수 있도록 허용하고 있다. 그래서, 다른 스레드에 동작할 때 이전 값과 최신 값에서 각각 32비트씩 읽어와서 아예 없는 값이 등장할 수도 있다.

락과 가시성

위의 문제를 막기 위해 synchronized 를 활용할 수 있다. A 라는 스레드가 synchronized 로 둘러싸인 코드에서 사용한 모든 값을, 같은 락을 사용하는 synchronized 로 둘러싸인 코드를 스레드 B가 실행할 때 안전하게 사용할 수 있다.

만약, synchronized 등으로 동기화하지 않으면 변수의 값을 제대로 읽어간다고 절대로 보장할 수 없다. 바꿔 말하면, 여러 스레드에서 사용하는 변수를 적당한 락으로 막아주지 않는다면, 스테일 상태에 쉽게 빠질 수 있다.

volatile 변수

자바 언어에서는 volatile 변수로 약간 동기화 기능을 제공할 수 있다. 다시 말에, volatile 로 선언된 변수의 값을 바꿨을 때 다른 변수에서 항상 최신값을 읽어갈 수 있게 해준다.

만약 특정 변수를 선언할 때 volatile 로 선언한다면, 컴파일러와 런타임 모두 ‘이 변수는 공유해 사용하므로 실행 순서를 재배치해서는 안 된다’ 라고 이해한다. 이 변수는 프로세서의 레지스터에도, 외부의 캐시에도 들어가지 않기 때문에 volatile 변수의 값을 읽으면 항상 다른 스레드가 보관해 둔 최신의 값을 읽을 수 있다. 예제를 살펴보자.

volatile boolean asleep;
// ...
    while(!asleep) countSomeSheep();

위의 코드는 특정 변수의 값을 확인해 반복문을 빠져나갈 상황인지 확인하는 예이다. 만약 위의 변수를 volatile 로 선언하지 않는다면, 다른 스레드가 asleep 의 값을 변경하였을 때 변경되었다는 상태를 확인하지 못할 수도 있다.

메모리 가시성의 입장에서 보면, volatile 변수를 사용하는 것은 synchronized 블록에 진입하는 것과 비슷한 상태에 해당한다. 하지만 그렇다고 해서, volatile 변수에 너무 의존하는 것은 좋지 않다. 이 변수만 사용하여 메모리 가시성을 확보하도록 작성한 코드는 synchronized 로 직접 동기화한 코드보다 훨씬 읽기 어렵고, 따라서 오류가 발생할 가능성도 높다.

일반적으로 volatile 변수는 위의 예제처럼 작업을 완료했다거나, 인터럽트가 걸리거나, 기타 상태를 보관하는 플래그 변수에 volatile 키워드를 지정한다.

정리하면 다음과 같다.

락을 사용하면 가시성과 연산의 단일성을 모두 보장받을 수 있다. 하지만 volatile 변수는 연산의 단일성을 보장하지 못하고 가시성만 보장한다.

volatile 이와 같은 상황에만 사용하면 좋다.

  • 변수에 값을 저장하는 작업이 해당 변수의 현재 값과 관련이 없거나, 해당 변수의 값을 변경하는 스레드가 하나만 존재
  • 해당 변수가 불변조건과 관련되어 있지 않다.
  • 해당 변수를 사용하는 동안에는 어떤 경우라도 락을 걸어 둘 필요가 없다.

공개와 유출

특정 객체를 현재 코드의 스코프 범위 밖에서 사용할 수 있도록 만들면 공개(published) 되었다고 한다.

  • 스코프 밖의 코드에서 볼 수 있는 변수에 스코프 내부의 객체에 대한 참조를 저장한다.
  • private 이 아닌 메소드에서 호출한 메소드가 스코프 내부의 객체에 대한 참조를 저장한다.
  • private 가 아닌 메소드에서 호출한 메소드가 내부에서 생성한 객체를 리턴한다.
  • 다른 클래스의 메소드로 객체를 넘겨준다.

위와 같은 상황이 발생하면, 객체가 공개되었다고 할 수 있다. 이와 같은 경우에, 의도적으로 객체를 공개하지는 않았지만 외부에서 사용할 수 있게 공개되었을 수 있다. 이 경우에는 누출(escaped) 되었다고 한다.

다음 예제를 확인해 보자

class UnsafeStats {
	private String[] states = new String[] {"AK", "AL"};
 
	public String[] getStates() {
		return states;
	}
}

private 으로 변수를 숨겼지만, getStates 를 통해 숨겨진 변수의 값을 직접적으로 변경할 수 있다. 즉, 객체를 공개했을 때 그 객체 내부의 private 이 아닌 변수나 메소드를 통해 불러올 수 있는 모든 객체는 함께 공개된다.

다른 스레드에서 공개된 객체를 사용해 실제로 작업을 하지 않기 때문에, 별 문제가 아니라고 생각할 수도 있다. 그러나, 어떤 객체건 일단 유출되고 나면 다른 스레드가 유출된 클래스를 의도적이건 의도적이지 않건 간에 반드시 잘못 사용할 수 있다고 가정하여야 한다.

객체 내부의 값이 공개되는 다른 케이스는 내부 클래스의 인스턴스를 외부에 공개하는 경우이다. 내부 클래스의 인스턴스는 부모 클래스에 대한 참조를 갖고 있기 때문에, 부모 클래스도 함께 외부로 공개된다.

생성 메소드 안정성

생성 메소드를 실행하는 과정에 this 변수에 외부로 노출될 수도 있다. 일반적으로 생성 메소드가 완전히 종료된 다음에 객체의 상태가 개발자가 예상한 대로 초기화되기 때문에, 생성 메소드가 실행되는 동안 해당 객체가 외부에 공개된다면, 정상적이지 않은 상태의 객체가 외부로 공개될 수 있다.

생성 메소드를 실행하는 도중에는 this 변수가 외부에 유출되지 않게 해야 한다.

위의 케이스에 대해, 가장 흔하게 저지르는 실수는 생성 메소드에서 스레드를 새로 만들에 시작시키는 것이다. 필요한 기능이 있다면 생성 메소드에서 스레드를 생성하는 것은 별 문제가 아니지만, 스레드를 생성과 동시에 시작시키는 것은 문제가 될 수 있다.

스레드를 바로 시작하면 그 스레드가 생성중인 객체의 외부 변수를 자유롭게 참조할 수 있기 때문이다. 스레드를 생성한다면, 바로 시작하기보다는 스레드를 시작하는 기능을 start 나 initialize 등의 메소드로 만들어 사용하는 편이 좋다. 새로 작성하는 클래스의 생성 메소드에서 이벤트 리스너를 등록하거나, 새로운 스레드를 시작하려면 다음과 같이 하는 편이 좋다. 디폴트 생성자를 private 으로 선언하고, 펙토리 메서드를 생성하여 사용하는 것이다.

public class SafeListener {
	private final EventListener listener;
 
	private SafeListener() {
		listener = new EventListener() {
			public void onEvent(Event e) {
				doSomething(e);
			}
		};
	}
 
	public static SafeListener newInstance(EventSource source) {
		SafeListener safe = new SafeListener();
		source.registerListener (safe.listener);
		return safe;
	}
}

이와 같이 작업하면 this 객체가 외부로 노출될 일이 없다.

스레드 한정

변경 가능한 객체를 공유하여 사용하는 경우에는 항상 동기화하여야 한다. 만약, 동기화하지 않아야 한다면 객체를 공유하여 사용하지 않을 수 밖에 없다. 다만 꼭 객체를 공유하여야 한다면 객체의 사용 범위를 한정(confine) 하는 방법이 있다. 특정 객체를 단일 스레드에서만 활용한다고 확신할 수 있다면, 해당 객체는 따로 동기화 할 필요가 없다.

예를 들면, 스윙에서는 이벤트 처리 스레드에 컴포넌트와 모델을 한정시켜, 스레드 안정성을 확보하고 있다. 또한 JDBC 에서도 같은 방법을 사용하고 있다.

JDBC 표준에 따르면 Connection 객체가 반드시 스레드 안정성을 확보하고 있어야 하는건 아니다. 일반적으로, 커넥션 풀에서 DB 커넥션을 가져와서 요청 하나를 처리한 다음에 다시 커넥션을 반환하는 과정을 거친다. 서블릿 요청이나 EJB 요청은 대부분 단일 스레드에서 처리되기 때문에, 한 번에 다수의 스레드가 하나의 커넥션을 참조하지 못하도록 한정할 수 있다.

스레드 한정 - 주먹구구식

스레드 한정 기법을 구현 단계에서 알아서 잘 처리해야 할 경우가 있을 수 있다. 만일 이런 경우에, 특정 스레드에 한정하려는 객체가 volatile 로 선언되어 있다면 변경 작업은 특정 스레드 한 곳에서만 할 수 있게 하고, 나머지는 읽기 작업만 가능하게 하여 스레드 한정을 구현할 수 있다.

이는 임시방편이니 가능하면 좀 더 안전한 스레드 한정 기법을 사용하자

스택 한정

스택 한정은 특정 객체를 코러 변수를 통해서만 사용할 수 있는, 특별한 경우의 스레드 한정 기법이다. 변수를 클래스 내부에 숨겨두면 상태 관리에 용이하고, 특정 스레드에 한정하기 쉽다. 로컬 변수는 이 두 가지를 모두 충족하는데, 현재 실행중인 스레드 내부의 스택에서만 존재하기 때문이다.

다음 예제를 보자

public int loadTheArk(Collection<Animal> candidates) {
	SortedSet<Animal> animals;
	int numPairs = 0;
	Animal candidate = null;
 
	// animal 변수는 메소드 한정이며, 유출되어서는 안된다.
	animals = new TreeSet<Animal>(new SpeciesGenderComparator());
	animals.addAll(candidates);
 
	for (Aniaml a : animals) {
	    if (candidates == null || !candidate.isPotentialMate(a)) {
	    	candidate = a;
	    }	else {
	    	ark.load(new AnimalPair(candidate, a));
	    	++numPair;
	    	candidate == null;
	    }
	}
 
	return numBairs;
}

TreeSet 클래스의 인스턴스를 만든 후, animals 에 보관한다. 그러면 TreeSet 인스턴스에 대한 참조가 정확하게 하나만 존재하고 로컬 변수에 보관하고 있기 때문에 현재 실행중인 스레드의 스택에 안전하게 보관된다. 그러나 animals 에 대한 참조를 외부에 공개한다면, 스택 한정 상태가 깨지게 되니 주의하여야 한다.

ThreadLocal

스레드 내부의 값과 객체를 연결해 스레드 한정 기법을 사용할 수 있도록 도와주는 방법으로 ThreadLocal 이 있다. ThreadLocal 클래스에는 get 과 set 이 있는데, 호출하는 클래스마다 각기 다른 값을 사용하도록 관리해준다.

스레드로컬 변수는 변경 가능한 싱글턴이나 전역 변수 등을 기반으로 설계되어 있는 구조에서 변수가 임의로 공유되는 상황을 막기 위해 사용하는 경우가 많다. 예를 들어, 단일스레드로 동작하는 애플리케이션에서 데이터베이스에 접속할 때 매번 Connection 인스턴스를 만들어내는 부담을 줄이고자 프로그램 시작 시점에 Connection 인스턴스를 하나 만들어 계속해서 사용한다고 가정해보자.

이 객체를 전역 변수로 만들어 사용하면 변수를 동기화하여야 한다. 그러나, Connection 인스턴스를 저장할 때 ThreadLocal 을 사용하면 스레드는 저마다 각자의 연결 객체를 갖게 된다.

private static ThreadLocal<Connection> connectionHolader = new ThreadLocal<Connection>() {
	public Connection initialValue() {
		return DriverManager.getConnection(DB_URL);
	}
};
 
public static Connection getConnection() {
	return connectionHolder.get();
}

이런 방법은 자주 호출되는 메소드에서 임시로 사용할 객체를 새로 생성하는 대신, 이미 만들어진 객체를 재활용할 때 많이 사용한다. 개념적으로 본다면, ThreadLocal 클래스는 Map<Thread, T> 라는 자료 구조로 구성되어 있고, Map<Thread, T> 에 스레드별 값을 보관한다고 생각할 수 있다. 물론 이렇게 구현되어 있다는 건 아니고, 스레드별 값은 실제로는 Thread 객체 자체에 저장되어 있고 스레드가 종료되면 스레드별 값으로 할당되어 있는 부분도 GC 가 처리한다.

ThreadLocal 은 애플리케이션 프레임워크를 구현할 때 많이 사용된다. 예를 들면, J2EE 컨테이너는 EJB 를 사용하는 동안 해당 스레드와 트랜잭션 컨텍스트를 연결해 관리한다. 이 때, static 으로 선언된 ThreadLocal 에 트랜젝션 컨텍스트를 넣어두면 편리하다.

이렇게 편리하긴 하지만, 전역 변수가 아니면서도 전역 변수처럼 동작하기 때문에, 프로그램의 구조가 허약해 질 수 있다. 또한 스레드마다 각기 값을 할당하는 것이기 때문에, 스레드 풀에서 스레드를 가져와 사용하는 경우에는 사용이 끝나면 해당 값을 삭제해주어야 한다.

불변성

지금까지 발생한 문제와 해결책들은 객체의 상태가 변한다는 가정 하에 생긴 문제들이다. 그런데 만약 객체의 상태가 변하지 않는다고 가정하면 어떨까? 지금까지 발생했던 문제들이 일순간에 사라진다.

불변 객체는 언제라도 스레드에 안전하다.

불변 객체는 다음 조건을 만족하여야 한다.

  1. 생성되고 난 이후에는 객체의 상태를 변경할 수 없다
  2. 내부의 모든 변수는 final 로 설정돼야 한다.
  3. 적절한 방법으로 생성돼야 한다(예를 들어 this 변수에 대한 참조가 외부로 유출되지 않아야 한다).

실행 중인 프로그램은 상태가 계속 변경되어야 하기 때문에, 불변 객체가 쓸모가 있을지에 대한 의문이 생길 수 있다. 그러나, 불변 객체는 쓰임새가 참으로 다양하다. 객체가 불변이라는 것과 참조가 불변이라는 것은 구분하여 생각해야 한다.

에를 들어 사용하는 데이터가 불변 객체에 들어있다고 해도, 해당 객체를 레퍼런스하는 변수에 다른 불변 객체를 생성하여 대입하는 방법으로 데이터를 변경할 수 있다.

final 변수

final 변수는 불변 객체를 생성할 때도 도움을 준다. final 을 지정한 변수의 값은 변경할 수 없다. 즉 불변객체가 되는데, 이를 volatile 키워드와 결합하여 사용함으로서 불변 객체를 안전하게 공개할 수 있다. 다음 코드를 보자

@Immutable
class OneValueCache {
	private final BigInteger lastNumber;
	private final BigInteger[] lastFactors; // 캐시
 
	public OneValueCache(BigInteger i, BigInteger [] factors) {
		lastNumber = i;
		lastFactors = Arrays.copyOf(factors, factors.length); // 새로운 객체를 생성한다.
	}
 
	// 새 객체를 생성하여 반환한다.
	public BigInteger[] getFactors(BigInteger i) {
		if (lastNumber == null || !lastNumber.equals(i)) {
			return null;
		} else {
			return Arrays.copyOf(lastFactors, lastFactors.length);
		}
	}
}

이는 인수분해를 하는 서블릿에서 사용할 클래스이다. 인수분해를 할 때 퍼포먼스를 높이기 위해 이전의 값을 cache 해 두도록 구현되어 있다. 이 경우 단일 연산으로 처리해야 하는 작업이 두 가지가 된다.

하나는 캐시 값을 보관하는 것이고, 다른 하나는 캐시된 값이 요청한 값에 해당하는 경우 캐시값을 읽어오는 작업이다. 만약 여리 개의 값이 단일하게 한꺼번에 움직여야 한다면, 위의 코드처럼 동일하게 움직여야 하는 한대 묶는 불변 객체를 만들어 사용하는 것이 좋다. 불변 객체에 해당하는 값을 모두 모아두면 경쟁 조건을 방지할 수 있고, 변수 값을 변경하면 새로운 변수를 생성하기 때문에 다른 스레드는 아무런 이상 없이 동작한다.

이제 위의 클래스를 사용하여 인수분해 서블릿을 구현해 보자

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
	private volatile OneValueCache cache = new OneValueCache(null, null);
 
	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = cache.getFactors(i);
 
		if(factors == null) {
			factors = factor(i);
			cache = new OneValueCache(i, factors);
		}
 
		encodeIntoResponse(resp, factors);
	}
}

OneValueCache 클래스가 불변인데다가, VolatileCachedFactorizer 클래스는 volatile 키워드를 사용하였으므로 시간적으로 가시성을 확보하였기 때문에 스레드 안전하다.

안전 공개

지금까지는 객체를 숨기는 방법에 대해 이야기하였다. 이제 객체를 공개하는 방법에 대해 이야기 할 것이다. 물론, 객체를 public 으로 공개하는 것은 올바르지 못한 방법이다. 이 값이 외부 스레드에 노출되면, 다양한 스레드에서 접근하여 값을 변경할 수 있기 때문이다.

적절하지 않은 공개 방법: 정상적인 객체도 문제를 일으킨다.

만약 객체의 생성 메소드가 완료되지 않은 상태라면 그 객체를 정상적으로 사용할 수 있을까? 그 객체는 비정상적인 상태로 사용될 가능성이 있고, 생성 메소드가 끝난 다음에 의도했던 대로 값이 세팅되지 않을 수 있다.

public class Holder {
	private int n;
 
	public Holder(int n) {
		this.n = n;
	}
 
	public void assertSanity() {
		if (n != n) {
			throw new AssertionError("This statement is false.");
		}
	}
}

Holder 객체를 다른 스레드가 사용할 수 있지만, 올바르게 동기화되어있지 않기 때문에 Holder 클래스는 올바르게 동기화되지 않았다고 할 수 있다. 객체를 올바르지 않게 공개하면 두 가지 문제점이 발생할 수 있다.

  1. holder 변수에 스테일 상태가 발생할 수 있다.
  2. 다른 스레드는 모두 holder 변수에서 정상적인 값을 가져갈 수 있지만 Holder 클래스의 입장에서는 스테일 상태에 빠질 수 있다.

불변 객체와 초기화 안정성

만약 불변 객체를 사용한다면, 추가적인 동기화 방법을 사용하지 않는다 해도 항상 안전하게 올바른 참조값을 사용할 수 있다. 안전하게 초기화를 진행하려면 불변 객체는 다음과 같은 요구사항을 충족하여야 한다.

  1. 상태를 변경할 수 없어야 하고
  2. 모든 필드의 값이 final 로 선언되어야 하며
  3. 적절한 방법으로 생성해야 한다

불변 객체는 별다른 동기화 방법을 적용하지 않았다 해도 어느 스레드에서건 마음껏 안전하게 사용할 수 있다. 불변 객체를 공개하는 부분에 동기화 처리를 하지 않았 해도 아무런 문제가 없다.

안전한 공개 방법의 특성

불변 객체가 아닌 객체는 모두 올바른 방법으로 안전하게 공개해야 하며, 대부분은 공개하는 스레드와 불러다 사용하는 스레드 양쪽 모두에 동기화 방법을 적용해야 한다.

객체를 안전하게 공개하려면 해당 객체에 대한 참조와 객체 내부의 상태를 외부의 스레드에서 동시에 볼 수 있어야 한다. 올바르게 생성 메소드가 실행되고 난 객체는 다음과 같은 방법으로 안전하게 공개할 수 있다.

  1. 객체에 대한 참조를 static 메소드에서 초기화시킨다.
  2. 객체에 대한 참조를 valatile 변수 혹은 AtomicReference 클래스에 보관한다.
  3. 객체에 대한 참조를 올바르게 생성된 클래스 내부의 final 변수에 보관한다.
  4. 락을 사용해 올바르게 막혀 있는 변수에 객체에 대한 참조를 보관한다.

또한 다음과 같이 선언할 수도 있다.

public static Holder holder = new Holder(42);

이는 가장 쉬우면서도 안전한 객체 공개 방법이다. static 초기화 방법은 JVM에서 클래스를 초기화하는 시점에 작업이 모두 진행되는데, JVM 내부에서 동기화가 맞춰져 있기 때문에 객체를 안전하게 공개할 수 있다.

결과적으로 불변인 객체

기술적으로 본다면 특정 객체가 불변일 수 없다고 해도, 한 번 공개된 이후에는 그 내용이 변경되지 않는다면 결과적으로 불변 객체라 볼 수 있다. 즉, 한번 공개한 다음에는 마치 불변 객체인 것처럼 쓰면 된다. 이와 같은 결과적인 불변 객체는 개발 과정도 간단하고 동기화 작업도 할 필요가 없기 때문에 성능 개선에 도움이 된다.

안전하게 공개한 결과적인 불변 객체는 별다른 동기화 작업 없이도 여러 스레드에서 안전하게 호출해 사용할 수 있다.

예를 들면 Date 클래스는 불변 객체가 아니라, 여러 스레드에서 사용하려면 락을 걸어야 한다. 그러나, 불변 객체인 것처럼 사용한다면 동기화 작업을 하지 않아도 된다. 다음 코드를 보자

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>());

이와 같은 코드에서 Map 에 들어간 Date 의 값을 변경하지 않는다면 위의 메소드로 선언한 것 만으로도 동기화 작업이 충분하다.

가변 객체

객체의 생성 메소드를 실행한 다음에 내용이 변경될 수 있다면, 공개하는 부분과 가변 객체를 사용하는 모든 부분에서 동기화 코드를 작성하여야 한다.

정리하면 다음과 같다.

가변성에 따라 객체를 공개할 때 필요한 점을 살펴보면 다음과 같다.

  1. 불변 객체는 어떤 방법으로 공개해도 아무 문제가 없다.
  2. 결과적으로 불변인 객체는 안전하게 공개해야 한다.
  3. 가변 객체는 안전하게 공개해야 하고, 스레드에서 안전하게 만들거나 락으로 동기화시켜야 한다.